/*
 * ByteStream.js
 *
 * Copyright (C) 2012 - Dr.NP
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see <http://www.gnu.org/licenses/>.
 */

/**
 * Binary stream operator for JavaScript
 * New Firefox && Chrome support only now.
 * 
 * @package bsp
 * @author Dr.NP <np@bsgroup.org>
 * @update 10/11/2012
 * @changelog
 * 		[10/11/2012] - Creation
 */

function ByteStream()
{
	// Typed array initialize length
	var base_len = 1024;
	
	this.ua = new Uint8Array(base_len);
	this.ua_len = base_len;
	this.data_len = 0;
	this.cursor = 0;
	this.mark_cursor = 0;
	
	this.bytesAvailable = function() {
		return this.data_len - this.cursor;
	}
	
	this.bytesRead = function() {
		return this.cursor - this.mask_cursor;
	}
	
	var endian_test = new ArrayBuffer(4);
	var et32 = new Uint32Array(endian_test);
	var et8 = new Uint8Array(endian_test);
	et32[0] = 1;
	if (et8[3] == 1)
	{
		// Big endian
		this.endian = 1;
	}
	
	else
	{
		this.endian = 0;
	}
	
	return;
}

// String byte length
ByteStream.prototype._real_len = function (s) {
	return encodeURI(s).split(/%..|./).length - 1;
}

// Append a Uint8Array
ByteStream.prototype._append = function (s) {
	if (this.ua_len - this.data_len < s.length)
	{
		// Enlarge array
		var i;
		var bit = 10;
		var new_len = (s.length + this.data_len)
		while (1)
		{
			if (1 == new_len >> bit)
			{
				break;
			}
			
			bit ++;
		}
		
		this.ua_len = 2 << bit;
		
		// New typed array
		var new_ua = new Uint8Array(this.ua_len);
		for (i = 0; i < this.data_len; i ++)
		{
			new_ua[i] = this.ua[i];
		}
		
		delete(this.ua);
		this.ua = new_ua;
	}
	
	for (i = 0; i < s.length; i ++)
	{
		this.ua[i + this.data_len] = s[i];
	}
	
	this.data_len += s.length;
	
	return;
}

// Reset read cursor to the beginning of buffer
ByteStream.prototype.reset = function() {
	this.cursor = 0;
	
	return;
}

ByteStream.prototype.mark = function() {
	this.mark_cursor = this.cursor;
	
	return;
}

// New UA
ByteStream.prototype.zero = function() {
	this.cursor = 0;
	this.data_len = 0;
	
	return;
}

// *** Set values ***
ByteStream.prototype.set_int8 = function(n) {
	var t = new Uint8Array(1);
	t[0] = n & 0xFF;
	this._append(t);
	
	return;
}

ByteStream.prototype.set_int16 = function(n) {
	var t = new Uint8Array(2);
	t[0] = (n >> 8) & 0xFF;
	t[1] = n & 0xFF;
	this._append(t);
	
	return;
}

ByteStream.prototype.set_int32 = function(n) {
	var t = new Uint8Array(4);
	t[0] = (n >> 24) & 0xFF;
	t[1] = (n >> 16) & 0xFF;
	t[2] = (n >> 8) & 0xFF;
	t[3] = n & 0xFF;
	this._append(t);
	
	return;
}

ByteStream.prototype.set_int64 = function(n) {
	var t = new Uint8Array(8);
	/*
	t[0] = (n >> 56) & 0xFF;
	t[1] = (n >> 48) & 0xFF;
	t[2] = (n >> 40) & 0xFF;
	t[3] = (n >> 32) & 0xFF;
	*/
	t[0] = 0;
	t[1] = 0;
	t[2] = 0;
	t[3] = 0;
	t[4] = (n >> 24) & 0xFF;
	t[5] = (n >> 16) & 0xFF;
	t[6] = (n >> 8) & 0xFF;
	t[7] = n & 0xFF;
	this._append(t);
	
	return;
}

ByteStream.prototype.set_float = function(n) {
	var ab = new ArrayBuffer(4);
	var t = new Uint8Array(ab);
	var f = new Float32Array(ab);
	f[0] = n;
	
	if (0 == this.endian)
	{
		// Swap all
		var tmp;
		tmp = t[0];
		t[0] = t[3];
		t[3] = tmp;
		tmp = t[1];
		t[1] = t[2];
		t[2] = tmp;
	}
	
	this._append(t);
	
	return;
}

ByteStream.prototype.set_double = function(n) {
	var ab = new ArrayBuffer(8);
	var t = new Uint8Array(ab);
	var d = new Float64Array(ab);
	d[0] = n;
	
	if (0 == this.endian)
	{
		// Swal all
		var tmp;
		tmp = t[0];
		t[0] = t[7];
		t[7] = tmp;
		tmp = t[1];
		t[1] = t[6];
		t[6] = tmp;
		tmp = t[2];
		t[2] = t[5];
		t[5] = tmp;
		tmp = t[3];
		t[3] = t[4];
		t[4] = tmp;
	}
	
	this._append(t);
	
	return;
}

ByteStream.prototype.set_utfstring = function(n) {
	var nlen = this._real_len(n);
	var t = new Uint8Array(nlen);
	var pos = 0;
	var curr = 0;
	while (pos < nlen && curr < n.length)
	{
		var code = n.charCodeAt(curr ++);
		if (code <= 0x7F)
		{
			t[pos ++] = code;
		}
		else if (code >= 0x80 && code <= 0x7FF)
		{
			t[pos ++] = (((code >> 6) & 0x1F) | 0xC0);
			t[pos ++] = ((code & 0x3F) | 0x80);
		}
		
		else
		{
			t[pos ++] = ((code >> 12) | 0xE0);
			t[pos ++] = (((code >> 6) & 0x3F) | 0x80);
			t[pos ++] = ((code & 0x3F) | 0x80);
		}
	}
	
	this._append(t);
	
	return;
}

ByteStream.prototype.set_arraybuffer = function(n) {
	var t = new Uint8Array(n);
	this._append(t);
	
	return;
}

// *** Get values ***
ByteStream.prototype.get_int8 = function() {
	var pos = arguments[0] ? arguments[0] : -1;
	if (pos < 0 || pos >= this.data_len)
	{
		pos = this.cursor;
	}
	
	this.cursor ++;
	
	return this.ua[pos];
}

ByteStream.prototype.get_int16 = function() {
	var pos = arguments[0] ? arguments[0] : -1;
	if (pos < 0 || pos >= this.data_len - 1)
	{
		pos = this.cursor;
	}
	
	this.cursor += 2;
	
	return (this.ua[pos] << 8) + 
			this.ua[pos + 1];
}

ByteStream.prototype.get_int32 = function() {
	var pos = arguments[0] ? arguments[0] : -1;
	if (pos < 0 || pos >= this.data_len - 3)
	{
		pos = this.cursor;
	}
	
	this.cursor += 4;
	
	return (this.ua[pos] << 24) + 
			(this.ua[pos + 1] << 16) + 
			(this.ua[pos + 2] << 8) + 
			this.ua[pos + 3];
}

ByteStream.prototype.get_int64 = function() {
	var pos = arguments[0] ? arguments[0] : -1;
	if (pos < 0 || pos >= this.data_len - 7)
	{
		pos = this.cursor;
	}
	
	this.cursor += 8;
	
	return (this.ua[pos] << 56) + 
			(this.ua[pos + 1] << 48) + 
			(this.ua[pos + 2] << 40) + 
			(this.ua[pos + 3] << 32) + 
			(this.ua[pos + 4] << 24) + 
			(this.ua[pos + 5] << 16) + 
			(this.ua[pos + 6] << 8) + 
			this.ua[pos + 7];
}

ByteStream.prototype.get_float = function() {
	var pos = arguments[0] ? arguments[0] : -1;
	if (pos < 0 || pos >= this.data_len - 3)
	{
		pos = this.cursor;
	}
	
	var ab = new ArrayBuffer(4);
	var t = new Float32Array(ab);
	var u8 = new Uint8Array(ab);
	u8[0] = this.ua[pos];
	u8[1] = this.ua[pos + 1];
	u8[2] = this.ua[pos + 2];
	u8[3] = this.ua[pos + 3];
	this.cursor += 4;
	
	return t[0];
}

ByteStream.prototype.get_double = function() {
	var pos = arguments[0] ? arguments[0] : -1;
	if (pos < 0 || pos >= this.data_len - 7)
	{
		pos = this.cursor;
	}
	
	var ab = new ArrayBuffer(8);
	var t = new Float64Array(ab);
	var u8 = new Uint8Array(ab);
	u8[0] = this.ua[pos];
	u8[1] = this.ua[pos + 1];
	u8[2] = this.ua[pos + 2];
	u8[3] = this.ua[pos + 3];
	u8[4] = this.ua[pos + 4];
	u8[5] = this.ua[pos + 5];
	u8[6] = this.ua[pos + 6];
	u8[7] = this.ua[pos + 7];
	this.cursor += 8;
	
	return t[0];
}

ByteStream.prototype.get_utfstring = function(len)
{
	var pos = arguments[1] ? arguments[1] : -1;
	if (pos < 0 || pos >= this.data_len)
	{
		pos = this.cursor;
	}
	
	// Decode UTF to unicode
	var ret = '';
	var i;
	var c, u1, u2, u3;
	var value;
	for (i = pos; i < this.data_len; i ++)
	{
		if ((i - pos) >= len)
		{
			break;
		}
		
		c = this.ua[i];
		if (c >= 0xC2 && c <= 0xCF)
		{
			// Two bytes
			u1 = this.ua[++ i];
			value = c & 0x1F;
			value = (value << 6) + (u1 & 0x3F);
		}
		
		else if (c >= 0xE0 && c <= 0xEF)
		{
			// Three bytes
			u1 = this.ua[++ i];
			u2 = this.ua[++ i];
			value = c & 0x1F;
			value = (value << 6) + (u1 & 0x3F);
			value = (value << 6) + (u2 & 0x3F);
		}
		
		else if (c >= 0xF0 && c <= 0xF4)
		{
			// Four bytes
			u1 = this.ua[++ i];
			u2 = this.ua[++ i];
			u3 = this.ua[++ i];
			value = c & 0x1F;
			value = (value << 6) + (u1 & 0x3F);
			value = (value << 6) + (u2 & 0x3F);
			value = (value << 6) + (u3 & 0x3F);
		}
		
		else
		{
			value = c;
		}
		
		ret += String.fromCharCode(value);
	}
	
	this.cursor += len;
	
	return ret;
}

ByteStream.prototype.get_arraybuffer = function(len) {
	var pos = arguments[1] ? arguments[1] : -1;
	if (pos < 0 || pos >= this.data_len - len + 1)
	{
		pos = this.cursor;
	}
	
	var t = new ArrayBuffer(len);
	var dv = new Uint8Array(t);
	var i;
	
	for (i = 0; i < len; i ++)
	{
		dv[i] = this.ua[i + pos];
	}
	
	this.cursor += len;
	
	return t;
}

// Debugger
ByteStream.prototype.debug_hex = function() {
	console.log("ByteStream debug info");
	console.log("Length: %d bytes, BufferSize: %d bytes, Read cursor: %d", this.data_len, this.ua_len, this.cursor);
	var i;
	var t = new Uint8Array(this.ua);
	var output = '';
	var byte_value;
	for (i = 0; i < this.data_len; i ++)
	{
		byte_value = t[i].toString(16);
		if (1 == byte_value.length)
		{
			byte_value = '0' + byte_value;
		}
		
		output += byte_value + ' ';
		if (i % 16 == 15)
		{
			console.log(output);
			output = '';
		}
	}
	
	console.log(output);
	
	return;
}
